package ru.ipo.problemsapi;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: ilya
 * Date: 13.04.11
 * Time: 16:23
 */
//TODO generalize manifest
public class ManifestedProblem implements Problem {

    private Problem problem;

    private static final String PATH_SEPARATOR = "/";

    private final static String MANIFEST = "MANIFEST";
    private final static String TAG_CONTENTS = "contents";
    private final static String TAG_ALIAS = "alias";
    private final static String TAG_TEACHER = "teacher";
    private final static String ATTR_PATH = "path";

    private final static String ATTR_TARGET = "target";
    private final Set<String> teacherResources = new HashSet<String>(); //set of paths
    private final Map<String, String> manifestResources = new HashMap<String, String>();
    private final Map<String, String> aliases = new HashMap<String, String>();

    /**
     * Creates a manifested problem, reads Manifest from MANIFEST resource
     * @param problem problem to wrap
     * @throws java.io.IOException if failed to find or read manifest
     */
    public ManifestedProblem(Problem problem) throws IOException {
        this(problem, MANIFEST);
    }

    /**
     * Creates a manifested problem, reads Manifest from the given resource
     * @param problem problem to wrap
     * @param manifestResourceName path to the resource with Manifest
     * @throws java.io.IOException if failed to find or read manifest
     */
    public ManifestedProblem(Problem problem, String manifestResourceName) throws IOException {
        this(problem, problem.getReadStream(manifestResourceName));
    }

    /**
     * Creates a manifested problem, reads Manifest from the given InputStream
     * @param problem problem to wrap
     * @param manifest input stream with manifest
     * @throws java.io.IOException if failed to find or read manifest
     */
    public ManifestedProblem(Problem problem, InputStream manifest) throws IOException {
        this.problem = problem;
        readManifest(manifest);
    }

    /**
     * Creates a manifested problem, reads Manifest from the given InputStream
     * @param manifest input stream with manifest
     * @throws java.io.IOException if failed to find or read manifest
     */
    public ManifestedProblem(InputStream manifest) throws IOException {
        this.problem = EmptyProblem.getInstance();
        readManifest(manifest);
    }

    /**
     * @param name path to the data
     * @return input stream with data
     */
    public InputStream getReadStream(String name) {
        //resolve alias
        String alias = aliases.get(name);
        if (alias != null)
            name = alias;

        //search for entry in manifest first
        String chars = manifestResources.get(name);
        if (chars != null)
            return new ByteArrayInputStream(chars.getBytes(new ExtendedProblem(this).getEncoding(name)));

        return problem.getReadStream(name);
    }

    public String getString(String name) {
        String alias = aliases.get(name);
        if (alias != null)
            name = alias;

        String s = manifestResources.get(name);
        if (s != null) return s;

        return problem.getString(name);
    }

    public boolean isTeacher(String name) {
        String[] dirs = name.split(PATH_SEPARATOR);

        StringBuilder p = new StringBuilder();
        for (String dir : dirs) {
            if (dir.length() == 0)
                continue;
            p.append(dir);
            if (teacherResources.contains(p.toString()))
                return true;
            p.append(PATH_SEPARATOR);
            if (teacherResources.contains(p.toString()))
                return true;
        }

        return false;
    }

    public List<String> listNames() {
        List<String> result = new ArrayList<String>();
        result.addAll(problem.listNames());
        result.addAll(aliases.keySet());
        result.addAll(manifestResources.keySet());
        return Collections.unmodifiableList(result);
    }

    private void readManifest(InputStream manifest) throws IOException {
        if (manifest == null)
            //throw new IOException("No manifest in problem archive");
            return;

        //analyze manifest as XML
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder builder = dbf.newDocumentBuilder();
            Document document = builder.parse(manifest);
            Element root = document.getDocumentElement();

            NodeList list;

            //read aliases
            list = root.getElementsByTagName(TAG_ALIAS);
            for (int i = 0; i < list.getLength(); i++) {
                Element alias = (Element) list.item(i);
                aliases.put(alias.getAttribute(ATTR_PATH), alias.getAttribute(ATTR_TARGET));
            }

            //read contents
            list = root.getElementsByTagName(TAG_CONTENTS);
            for (int i = 0; i < list.getLength(); i++) {
                Element contents = (Element) list.item(i);
                String text = contents.getTextContent();
                manifestResources.put(
                        contents.getAttribute(ATTR_PATH),
                        text
                );
            }

            //read teacher resources
            list = root.getElementsByTagName(TAG_TEACHER);
            for (int i = 0; i < list.getLength(); i++) {
                Element teacher = (Element) list.item(i);
                teacherResources.add(teacher.getAttribute(ATTR_PATH));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            throw new IOException("Failed to read manifest");
        } catch (SAXException e) {
            throw new IOException("Error in manifest file");
        } finally {
            manifest.close();
        }
    }
}
